/*
 * lombo-cpufreq.c - Driver of LomboTech CPUfreq
 *
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/cpufreq.h>
#include <linux/cpu.h>
#include <linux/cpu_cooling.h>
#include <linux/pm_opp.h>
#include <linux/energy_model.h>
#include <linux/of.h>
#include <linux/clk.h>
#include <linux/err.h>
#include <linux/pwm.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/of_address.h>
#include <linux/delay.h>

#include <mach/debug.h>
#include <mach/common.h>
#include "csp/cpu_adpll.h"

#undef MOD_NAME
#define MOD_NAME "CPUFREQ"

#ifdef CONFIG_ARCH_LOMBO_N7V3
#define CPU_CLK_SRC_PRCM	0
#define CPU_CLK_SRC_CPU_ADPLL	1

static s32 cpu_initial_freq;
static s32 cpu_initial_volt = 900;
static u32 cpu_fixed_volt = 0;

void lombo_arm_pm_idle(void)
{
	local_irq_disable();
	csp_set_cpu_clk_src_sel(CPU_CLK_SRC_PRCM);

	asm volatile("wfi");

	csp_set_cpu_clk_src_sel(CPU_CLK_SRC_CPU_ADPLL);
	local_irq_enable();
}
#endif

#ifdef CONFIG_LOMBO_CPU_FREQ_AVS
/* @axnu_ready_flag: check if axnu driver is ready */
unsigned int axnu_ready_flag;
EXPORT_SYMBOL(axnu_ready_flag);

BLOCKING_NOTIFIER_HEAD(volt_charge_notifier_list);

int register_volt_charge_notifier(struct notifier_block *nb)
{
	return blocking_notifier_chain_register(&volt_charge_notifier_list, nb);
}
EXPORT_SYMBOL(register_volt_charge_notifier);

static u32 set_voltage_pwm(struct lombo_cpufreq *l_cpufreq, u32 volt)
{
	int period_ns = 10000;
	int ret;
	u32 duty, duty_ns;

	if ((l_cpufreq->plat_version == 0) && (PWM_PARAM_VERSION == 0)) {
		/* volt = (-1099) * D + 1100 */
		duty = (1100 - volt) * 100 / 1099;
		duty_ns = period_ns * duty / 100;

	} else {
		/* volt = (-247) * D + 1020 */
		duty = (1020 - volt) * 100 / 247;
		duty_ns = period_ns * duty / 100;
	}

	ret = pwm_config(l_cpufreq->cpu_pwm, duty_ns, period_ns);
	if (ret)
		PRT_ERR("diff_duty_ns:%d, duty_ns:%d\n", duty_ns, period_ns);

	return ret;
}

/*
 * -----------------------------------------------------------
 * void cpu_volt_cfg_pwm(u32 volt)
 * set the voltage of CPU, voltage unit: mV
 * eg: cpu_volt_cfg_pwm(800); set the CPU voltage 800mv
 * -----------------------------------------------------------
 */
static u32 cpu_volt_cfg_pwm(struct lombo_cpufreq *l_cpufreq, u32 volt)
{
	/* if config cpu vdd min voltage, set min voltage */
	if (volt <= l_cpufreq->min_voltage)
		volt = l_cpufreq->min_voltage;

#ifdef CONFIG_ARCH_LOMBO_N7V3
	if (volt <= 850UL)
		volt = 850;
#else
	if (volt <= 800UL)
		volt = 800;
#endif
	else if (volt >= 1000UL)
		volt = 1000;

	if (l_cpufreq->share_volt_flag && axnu_ready_flag) {
		blocking_notifier_call_chain(&volt_charge_notifier_list,
						volt, l_cpufreq);

		wait_for_completion(&l_cpufreq->volt_completion);
		/* reinitialize xfer_completion */
		reinit_completion(&l_cpufreq->volt_completion);
	}

	return set_voltage_pwm(l_cpufreq, volt);
}

/* tar_freq unit: MHz */
static u32 set_cpu_target_volt_avs(struct lombo_cpufreq *l_cpufreq,
							s32 tar_freq)
{
	s32 cpu_volt;

	/*
	 * use MHz to caculate
	 * cpu_freq / cpu_volt = 75MHz / 25mV
	 * 1000Khz / 1000 = 1Mhz
	 */
	cpu_volt = cpu_initial_volt +
		(tar_freq - cpu_initial_freq) / 1000 * 25 / 75;

	return cpu_volt_cfg_pwm(l_cpufreq, cpu_volt);
}


/* input freq is in KHz */
int lombo_set_freq(unsigned long old_freq, unsigned long target_freq,
			struct lombo_cpufreq *l_cpufreq)
{
	int ret;

	ret = set_cpu_target_volt_avs(l_cpufreq, (s32)target_freq);
	if (ret)
		PRT_ERR("set target volt error!\n");

	return ret;
}
EXPORT_SYMBOL_GPL(lombo_set_freq);
#endif

void lombo_get_freq(unsigned int *freq)
{
	struct cpufreq_policy *policy = cpufreq_cpu_get_raw(0);
	struct lombo_cpufreq *l_cpufreq = policy->driver_data;

	mutex_lock(&l_cpufreq->cpu_mutex);
	*freq = csp_get_cpu_rate(l_cpufreq->ctrl_base);
	mutex_unlock(&l_cpufreq->cpu_mutex);
}

static unsigned int lombo_cpufreq_get(unsigned int cpu)
{
	struct cpufreq_policy *policy = cpufreq_cpu_get_raw(cpu);
	struct lombo_cpufreq *l_cpufreq = policy->driver_data;
	static unsigned int freq;
	int index;
	unsigned int newfreq;

	mutex_lock(&l_cpufreq->cpu_mutex);
	freq = csp_get_cpu_rate(l_cpufreq->ctrl_base);
	index = cpufreq_frequency_table_target(policy, freq, CPUFREQ_RELATION_H);
	newfreq = policy->freq_table[index].frequency;
	mutex_unlock(&l_cpufreq->cpu_mutex);

	return newfreq;
}


/**
 * lombo_cpufreq_set - set target frequency to cpu.
 * @policy: pointer to cpufreq policy.
 * @index: target frequency index in policy freq_bable.
 *
 * Return 0 if success, !0 error.
 */
static int lombo_cpufreq_set(struct cpufreq_policy *policy, unsigned int index)
{
	struct lombo_cpufreq *l_cpufreq = policy->driver_data;
	unsigned long freq = policy->freq_table[index].frequency;
	int ret = 0;

	if (cpu_fixed_volt)
		return 0;

#if defined(CONFIG_LOMBO_CPU_FREQ_AVS)
	policy->cur = freq;
	ret = dev_pm_opp_set_rate(l_cpufreq->cpu_dev, l_cpufreq, freq * 1000);
#elif defined(CONFIG_LOMBO_CPU_ADPLL_NORMAL)
	ret = dev_pm_opp_set_rate(l_cpufreq->cpu_dev,
				l_cpufreq->ctrl_base, freq * 1000);
#else
	ret = dev_pm_opp_set_rate(l_cpufreq->cpu_dev, freq * 1000);
#endif

	if (!ret) {
		arch_set_freq_scale(policy->related_cpus, freq,
				    policy->cpuinfo.max_freq);
	}

	return ret;
}


#if !defined(CONFIG_LOMBO_CPU_PRCM)
int lombo_set_adpll(void __iomem *base, unsigned long target_freq)
{
	return csp_set_cpu_rate(base, target_freq);
}

/* opp core.c get current CPU frequency */
unsigned int lombo_get_adpll_rate(void __iomem *base)
{
	return csp_get_cpu_rate(base);
}

#ifdef CONFIG_LOMBO_CPU_FREQ_AVS
static int cpu_pwm_init(struct lombo_cpufreq *l_cpufreq)
{
	int i, ret, duty_ns;
	s32 freq = 0;
	u32 temp;

	/* set vdd_cpu/gpu/npu default as 900mV */
	duty_ns = set_voltage_pwm(l_cpufreq, cpu_initial_volt);

	/* enable pwm */
	ret = pwm_enable(l_cpufreq->cpu_pwm);
	if (ret)
		PRT_ERR("cpu pwm_enable fail! ret:%d\n", ret);
	/* wait voltage modified take effect */
	mdelay(2);

	/* get the actual initial freq of CPU */
	for (i = 0; i < 5; i++) {
		temp = csp_get_cpu_rate(l_cpufreq->ctrl_base);
		freq = freq + (s32)temp;
	}

	/* record the init freq (Khz) */
	cpu_initial_freq = freq / 5;
	PRT_INFO("cpu_initial_freq = %dMhz", cpu_initial_freq / 1000);

	if (cpu_fixed_volt) {
		PRT_INFO("The cpu voltage is set to a fixed volt of %dmv.\n",
						l_cpufreq->fixed_volt);
		ret = cpu_volt_cfg_pwm(l_cpufreq, l_cpufreq->fixed_volt);
		if (ret)
			PRT_ERR("set init fixed voltage error!\n");
	}

	if (l_cpufreq->share_volt_flag)
		init_completion(&l_cpufreq->volt_completion);

	return ret;
}
#endif
#endif

/**
 * lombo_cpufreq_init - initialise a policy;
 * @policy:  cpu frequency policy;
 *
 * Return 0 if success, !0 error;
 */
static int lombo_cpufreq_init(struct cpufreq_policy *policy)
{
	struct em_data_callback em_cb = EM_DATA_CB(of_dev_pm_opp_get_cpu_power);
	struct cpufreq_frequency_table *freq_table;
	struct lombo_cpufreq *l_cpufreq;
	struct device *cpu_dev;
	struct device_node *np;
	struct device_node *opp_np;
	unsigned int transition_latency;
	bool fallback = false;
	int ret, nr_opp;
#ifdef CONFIG_OLD_PWM_PARAM
	u32 p1;
#endif

	cpu_dev = get_cpu_device(policy->cpu);
	if (!cpu_dev) {
		pr_err("failed to get cpu%d device\n", policy->cpu);
		return -ENODEV;
	}

	ret = dev_pm_opp_of_get_sharing_cpus(cpu_dev, policy->cpus);
	if (ret) {
		if (ret != -ENOENT)
			goto out;

		if (dev_pm_opp_get_sharing_cpus(cpu_dev, policy->cpus))
			fallback = true;
	}

	l_cpufreq = kzalloc(sizeof(*l_cpufreq), GFP_KERNEL);
	if (!l_cpufreq) {
		ret = -ENOMEM;
		goto out;
	}

	np = cpu_dev->of_node;

	/*
	 * Get opp table np.
	 * Returns opp descriptor node for a device,
	 * caller must do of_node_put()
	 */
	opp_np = dev_pm_opp_of_get_opp_desc_node(cpu_dev);
	if (IS_ERR(opp_np)) {
		PRT_ERR("get opp_np fail!\n");
		goto out;
	}

	/* caller must do of_node_put() */
	of_node_put(opp_np);

	l_cpufreq->ctrl_base = of_iomap(opp_np, 0);
	if (IS_ERR(l_cpufreq->ctrl_base)) {
		PRT_ERR("%s io map fail\n", __func__);
		goto out_node_put;
	}

	csp_cpu_init(l_cpufreq->ctrl_base);
	mutex_init(&l_cpufreq->cpu_mutex);

#if defined(CONFIG_LOMBO_CPU_PRCM)
	l_cpufreq->clk_cpu = of_clk_get(np, 1);
	if (IS_ERR(l_cpufreq->clk_cpu)) {
		ret = PTR_ERR(l_cpufreq->clk_cpu);
		PRT_ERR("get cpu clock failed\n");
		goto out_node_put;
	}
	l_cpufreq->clk_pll = of_clk_get(np, 0);
	if (IS_ERR(l_cpufreq->clk_pll)) {
		ret = PTR_ERR(l_cpufreq->clk_pll);
		PRT_ERR("get cpu pll failed\n");
		goto out_clk_cpu_put;
	}

	ret = clk_set_parent(l_cpufreq->clk_cpu, l_cpufreq->clk_pll);
	if (ret) {
		PRT_ERR("set cpu parent clk failed\n");
		goto out_clk_pll_put;
	}

	/* enable cpu clk */
	ret = clk_prepare_enable(l_cpufreq->clk_cpu);
	if (ret) {
		PRT_ERR("enable cpu clock failed\n");
		goto out_clk_pll_put;
	}

	policy->clk = l_cpufreq->clk_pll;
#elif defined(CONFIG_LOMBO_CPU_FREQ_AVS)
#ifdef CONFIG_OLD_PWM_PARAM
	lombo_func2(&p1, &l_cpufreq->plat_version);
#endif

	ret = of_property_read_u32(opp_np, "min_voltage", &l_cpufreq->min_voltage);
	if (ret) {
		PRT_INFO("cpu vdd used default min voltage.\n");
		l_cpufreq->min_voltage = 0;
	} else
		PRT_INFO("The lower limit of CPU voltage is %dmv.\n",
					l_cpufreq->min_voltage);

	of_property_read_u32(opp_np, "fixed_voltage", &l_cpufreq->fixed_volt);
	if (l_cpufreq->fixed_volt > 0)
		cpu_fixed_volt = 1;

	of_property_read_u32(opp_np, "share_volt_flag", &l_cpufreq->share_volt_flag);
	if (l_cpufreq->share_volt_flag > 0)
		PRT_INFO("The voltage share of cpu vdd and npu vdd.\n");

	l_cpufreq->ptl = devm_pinctrl_get(cpu_dev);
	if (IS_ERR(l_cpufreq->ptl) || l_cpufreq->ptl == NULL)
		PRT_ERR("get pinctrl fail\n");
	else {
		l_cpufreq->pins = pinctrl_lookup_state(l_cpufreq->ptl,
								"default");
		if (IS_ERR(l_cpufreq->pins) || l_cpufreq->pins == NULL)
			PRT_ERR("set normal pinctrl fail\n");
		else {
			pinctrl_select_state(l_cpufreq->ptl, l_cpufreq->pins);
			PRT_INFO("cpu set pwm normal pin success\n");
		}

#ifdef CONFIG_PM
		l_cpufreq->suspend_pins = pinctrl_lookup_state(l_cpufreq->ptl,
								"suspend");
		if (IS_ERR(l_cpufreq->suspend_pins) ||
				l_cpufreq->suspend_pins == NULL)
			PRT_ERR("get suspend pinctrl fail\n");
#endif
	}

	mutex_lock(&l_cpufreq->cpu_mutex);
	l_cpufreq->cpu_pwm = of_pwm_get(np, NULL);
	cpu_pwm_init(l_cpufreq);
	policy->cur = csp_get_cpu_rate(l_cpufreq->ctrl_base);
	mutex_unlock(&l_cpufreq->cpu_mutex);
#endif
	/*
	 * Initialize OPP tables for all policy->cpus. They will be shared by
	 * all CPUs which have marked their CPUs shared with OPP bindings.
	 *
	 * For platforms not using operating-points-v2 bindings, we do this
	 * before updating policy->cpus. Otherwise, we will end up creating
	 * duplicate OPPs for policy->cpus.
	 *
	 * OPPs might be populated at runtime, don't check for error here
	 */
	if (!dev_pm_opp_of_cpumask_add_table(policy->cpus))
		l_cpufreq->have_static_opps = true;

	/*
	 * But we need OPP table to function so if it is not there let's
	 * give platform code chance to provide it for us.
	 */
	ret = dev_pm_opp_get_opp_count(cpu_dev);
	if (ret <= 0) {
		dev_dbg(cpu_dev, "OPP table is not ready, deferring probe\n");
		ret = -EPROBE_DEFER;
#ifdef CONFIG_LOMBO_CPU_PRCM
		goto out_clk_pll_put;
#else
		goto out_node_put;
#endif
	}
	nr_opp = ret;

	if (fallback) {
		cpumask_setall(policy->cpus);

		ret = dev_pm_opp_set_sharing_cpus(cpu_dev, policy->cpus);
		if (ret)
			PRT_ERR("failed to mark OPPs as shared: %d\n", ret);
	}

	ret = dev_pm_opp_init_cpufreq_table(cpu_dev, &freq_table);
	if (ret) {
		PRT_ERR("failed to init cpufreq table: %d\n", ret);
#ifdef CONFIG_LOMBO_CPU_PRCM
		goto out_clk_pll_put;
#else
		goto out_node_put;
#endif
	}

	l_cpufreq->cpu_dev = cpu_dev;
	policy->driver_data = l_cpufreq;
	policy->freq_table = freq_table;

	policy->suspend_freq = dev_pm_opp_get_suspend_opp_freq(cpu_dev) / 1000;

	/* Support turbo/boost mode */
	if (policy_has_boost_freq(policy)) {
		/* This gets disabled by core on driver unregister */
		ret = cpufreq_enable_boost_support();
		if (ret)
			goto out_free_cpufreq_table;
	}

	transition_latency = dev_pm_opp_get_max_transition_latency(cpu_dev);
	if (!transition_latency)
		transition_latency = CPUFREQ_ETERNAL;

	policy->cpuinfo.transition_latency = transition_latency;
	policy->dvfs_possible_from_any_cpu = true;

	em_register_perf_domain(policy->cpus, nr_opp, &em_cb);

	return 0;

out_free_cpufreq_table:
	dev_pm_opp_free_cpufreq_table(cpu_dev, &freq_table);

#ifdef CONFIG_LOMBO_CPU_PRCM
out_clk_pll_put:
	if (l_cpufreq->have_static_opps)
		dev_pm_opp_of_cpumask_remove_table(policy->cpus);
	clk_put(l_cpufreq->clk_pll);
out_clk_cpu_put:
	clk_put(l_cpufreq->clk_cpu);
#endif

out_node_put:
	of_node_put(np);
	of_node_put(opp_np);
	kfree(l_cpufreq);
out:
	return ret;
}

static void lombo_cpufreq_ready(struct cpufreq_policy *policy)
{
	struct lombo_cpufreq *l_cpufreq = policy->driver_data;

	l_cpufreq->cdev = of_cpufreq_cooling_register(policy);
}

static int lombo_cpufreq_exit(struct cpufreq_policy *policy)
{
	struct lombo_cpufreq *l_cpufreq = policy->driver_data;

	cpufreq_cooling_unregister(l_cpufreq->cdev);
	dev_pm_opp_free_cpufreq_table(l_cpufreq->cpu_dev, &policy->freq_table);
	if (l_cpufreq->have_static_opps)
		dev_pm_opp_of_cpumask_remove_table(policy->related_cpus);

#ifdef CONFIG_LOMBO_CPU_PRCM
	clk_put(policy->clk);
#endif
	kfree(l_cpufreq);

	return 0;
}

#ifdef CONFIG_PM
int lombo_cpufreq_suspend(struct cpufreq_policy *policy)
{
#ifdef CONFIG_LOMBO_CPU_FREQ_AVS
	struct lombo_cpufreq *l_cpufreq = policy->driver_data;

	pwm_disable(l_cpufreq->cpu_pwm);

	if ((l_cpufreq->plat_version != 0) || (PWM_PARAM_VERSION == 1))
		if (!(IS_ERR(l_cpufreq->suspend_pins)))
			pinctrl_select_state(l_cpufreq->ptl,
						l_cpufreq->suspend_pins);

#endif

	return 0;
}

int lombo_cpufreq_resume(struct cpufreq_policy *policy)
{
	int ret = 0;
#ifdef CONFIG_LOMBO_CPU_FREQ_AVS
	struct lombo_cpufreq *l_cpufreq = policy->driver_data;

	if ((l_cpufreq->plat_version != 0) || (PWM_PARAM_VERSION == 1))
		if (!(IS_ERR(l_cpufreq->pins)))
			pinctrl_select_state(l_cpufreq->ptl, l_cpufreq->pins);

	ret = pwm_enable(l_cpufreq->cpu_pwm);
#endif

	return ret;
}
#endif

static struct cpufreq_driver lombo_cpufreq_driver = {
	.name   = "cpufreq-lombo",
	.flags  = CPUFREQ_STICKY | CPUFREQ_NEED_INITIAL_FREQ_CHECK,
	.init   = lombo_cpufreq_init,
	.verify = cpufreq_generic_frequency_table_verify,
	.target_index = lombo_cpufreq_set,
	.get    = lombo_cpufreq_get,
	.attr   = cpufreq_generic_attr,
	.ready	= lombo_cpufreq_ready,
	.exit	= lombo_cpufreq_exit,
#ifdef CONFIG_PM
	.suspend = lombo_cpufreq_suspend,
	.resume = lombo_cpufreq_resume,
#endif
};

static int __init lombo_cpufreq_initcall(void)
{
	int ret;

	ret = cpufreq_register_driver(&lombo_cpufreq_driver);
	if (ret)
		PRT_ERR("cpufreq register failed ,ret = %d\n", ret);

	return ret;
}
fs_initcall(lombo_cpufreq_initcall);

MODULE_AUTHOR("lomboswer@lombotech.com>");
MODULE_DESCRIPTION("Cpufreq Driver for Lombo SoCs");
MODULE_LICENSE("GPL");
